Оператору связи «Ниединогоразрыва.ком» необходимо предсказывать отток клиентов. Если согласно предсказанию, пользователь планирует уйти, ему должны быть предложены промокоды и специальные условия. Для предсказания требуется разработать качественную модель машинного обучения с метрикой ROC-AUC на тестовой выборке в 0.85. Помимо ROC-AUC необходимо предоставить интерпретируемую метрику, такие как accuracy и матрица ошибок.
Заказчик предоставил следующие данные:
Цель работы - построить модель машинного обучения, предсказывающая уход клиентов оператора, при чем метрика ROC-AUC на тестовой выборке должна составлять не менее 0.85.
Задачи:
# Загрузка библиотек, необходимых в проекте:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import re
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import mean_absolute_error, roc_auc_score, accuracy_score, confusion_matrix
from sklearn.model_selection import cross_val_score
from sklearn.preprocessing import FunctionTransformer, OneHotEncoder, OrdinalEncoder, StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score, RandomizedSearchCV, GridSearchCV, train_test_split
from sklearn.metrics import roc_curve
from sklearn.pipeline import Pipeline, FeatureUnion
from sklearn.compose import ColumnTransformer
!pip install phik
import phik
from phik import resources
from phik.binning import bin_data
from phik.report import plot_correlation_matrix
!pip install catboost
import catboost as cat
from catboost import CatBoostClassifier, Pool, cv
import warnings
warnings.filterwarnings("ignore")
!pip install lightgbm
import lightgbm
from lightgbm import LGBMClassifier
Requirement already satisfied: phik in c:\users\user\anaconda3\lib\site-packages (0.12.3) Requirement already satisfied: numpy>=1.18.0 in c:\users\user\anaconda3\lib\site-packages (from phik) (1.24.3) Requirement already satisfied: scipy>=1.5.2 in c:\users\user\anaconda3\lib\site-packages (from phik) (1.10.1) Requirement already satisfied: pandas>=0.25.1 in c:\users\user\anaconda3\lib\site-packages (from phik) (1.5.3) Requirement already satisfied: matplotlib>=2.2.3 in c:\users\user\anaconda3\lib\site-packages (from phik) (3.7.1) Requirement already satisfied: joblib>=0.14.1 in c:\users\user\anaconda3\lib\site-packages (from phik) (1.2.0) Requirement already satisfied: contourpy>=1.0.1 in c:\users\user\anaconda3\lib\site-packages (from matplotlib>=2.2.3->phik) (1.0.5) Requirement already satisfied: cycler>=0.10 in c:\users\user\anaconda3\lib\site-packages (from matplotlib>=2.2.3->phik) (0.11.0) Requirement already satisfied: fonttools>=4.22.0 in c:\users\user\anaconda3\lib\site-packages (from matplotlib>=2.2.3->phik) (4.25.0) Requirement already satisfied: kiwisolver>=1.0.1 in c:\users\user\anaconda3\lib\site-packages (from matplotlib>=2.2.3->phik) (1.4.4) Requirement already satisfied: packaging>=20.0 in c:\users\user\anaconda3\lib\site-packages (from matplotlib>=2.2.3->phik) (23.0) Requirement already satisfied: pillow>=6.2.0 in c:\users\user\anaconda3\lib\site-packages (from matplotlib>=2.2.3->phik) (9.4.0) Requirement already satisfied: pyparsing>=2.3.1 in c:\users\user\anaconda3\lib\site-packages (from matplotlib>=2.2.3->phik) (3.0.9) Requirement already satisfied: python-dateutil>=2.7 in c:\users\user\anaconda3\lib\site-packages (from matplotlib>=2.2.3->phik) (2.8.2) Requirement already satisfied: importlib-resources>=3.2.0 in c:\users\user\anaconda3\lib\site-packages (from matplotlib>=2.2.3->phik) (5.2.0) Requirement already satisfied: pytz>=2020.1 in c:\users\user\anaconda3\lib\site-packages (from pandas>=0.25.1->phik) (2022.7) Requirement already satisfied: zipp>=3.1.0 in c:\users\user\anaconda3\lib\site-packages (from importlib-resources>=3.2.0->matplotlib>=2.2.3->phik) (3.11.0) Requirement already satisfied: six>=1.5 in c:\users\user\anaconda3\lib\site-packages (from python-dateutil>=2.7->matplotlib>=2.2.3->phik) (1.16.0) Requirement already satisfied: catboost in c:\users\user\anaconda3\lib\site-packages (1.2) Requirement already satisfied: graphviz in c:\users\user\anaconda3\lib\site-packages (from catboost) (0.20.1) Requirement already satisfied: matplotlib in c:\users\user\anaconda3\lib\site-packages (from catboost) (3.7.1) Requirement already satisfied: numpy>=1.16.0 in c:\users\user\anaconda3\lib\site-packages (from catboost) (1.24.3) Requirement already satisfied: pandas>=0.24 in c:\users\user\anaconda3\lib\site-packages (from catboost) (1.5.3) Requirement already satisfied: scipy in c:\users\user\anaconda3\lib\site-packages (from catboost) (1.10.1) Requirement already satisfied: plotly in c:\users\user\anaconda3\lib\site-packages (from catboost) (5.9.0) Requirement already satisfied: six in c:\users\user\anaconda3\lib\site-packages (from catboost) (1.16.0) Requirement already satisfied: python-dateutil>=2.8.1 in c:\users\user\anaconda3\lib\site-packages (from pandas>=0.24->catboost) (2.8.2) Requirement already satisfied: pytz>=2020.1 in c:\users\user\anaconda3\lib\site-packages (from pandas>=0.24->catboost) (2022.7) Requirement already satisfied: contourpy>=1.0.1 in c:\users\user\anaconda3\lib\site-packages (from matplotlib->catboost) (1.0.5) Requirement already satisfied: cycler>=0.10 in c:\users\user\anaconda3\lib\site-packages (from matplotlib->catboost) (0.11.0) Requirement already satisfied: fonttools>=4.22.0 in c:\users\user\anaconda3\lib\site-packages (from matplotlib->catboost) (4.25.0) Requirement already satisfied: kiwisolver>=1.0.1 in c:\users\user\anaconda3\lib\site-packages (from matplotlib->catboost) (1.4.4) Requirement already satisfied: packaging>=20.0 in c:\users\user\anaconda3\lib\site-packages (from matplotlib->catboost) (23.0) Requirement already satisfied: pillow>=6.2.0 in c:\users\user\anaconda3\lib\site-packages (from matplotlib->catboost) (9.4.0) Requirement already satisfied: pyparsing>=2.3.1 in c:\users\user\anaconda3\lib\site-packages (from matplotlib->catboost) (3.0.9) Requirement already satisfied: importlib-resources>=3.2.0 in c:\users\user\anaconda3\lib\site-packages (from matplotlib->catboost) (5.2.0) Requirement already satisfied: tenacity>=6.2.0 in c:\users\user\anaconda3\lib\site-packages (from plotly->catboost) (8.2.2) Requirement already satisfied: zipp>=3.1.0 in c:\users\user\anaconda3\lib\site-packages (from importlib-resources>=3.2.0->matplotlib->catboost) (3.11.0) Requirement already satisfied: lightgbm in c:\users\user\anaconda3\lib\site-packages (3.3.5) Requirement already satisfied: wheel in c:\users\user\anaconda3\lib\site-packages (from lightgbm) (0.38.4) Requirement already satisfied: numpy in c:\users\user\anaconda3\lib\site-packages (from lightgbm) (1.24.3) Requirement already satisfied: scipy in c:\users\user\anaconda3\lib\site-packages (from lightgbm) (1.10.1) Requirement already satisfied: scikit-learn!=0.22.0 in c:\users\user\anaconda3\lib\site-packages (from lightgbm) (1.3.0) Requirement already satisfied: joblib>=1.1.1 in c:\users\user\anaconda3\lib\site-packages (from scikit-learn!=0.22.0->lightgbm) (1.2.0) Requirement already satisfied: threadpoolctl>=2.0.0 in c:\users\user\anaconda3\lib\site-packages (from scikit-learn!=0.22.0->lightgbm) (2.2.0)
!pip install -U scikit-learn
Requirement already satisfied: scikit-learn in c:\users\user\anaconda3\lib\site-packages (1.3.0) Requirement already satisfied: numpy>=1.17.3 in c:\users\user\anaconda3\lib\site-packages (from scikit-learn) (1.24.3) Requirement already satisfied: scipy>=1.5.0 in c:\users\user\anaconda3\lib\site-packages (from scikit-learn) (1.10.1) Requirement already satisfied: joblib>=1.1.1 in c:\users\user\anaconda3\lib\site-packages (from scikit-learn) (1.2.0) Requirement already satisfied: threadpoolctl>=2.0.0 in c:\users\user\anaconda3\lib\site-packages (from scikit-learn) (2.2.0)
# Загрузим данные из файла:
# contract_new
try:
contract_new = pd.read_csv('')
except:
try:
contract_new = pd.read_csv('D:/YandexDisk/Яндекс практикум/18 Финальный проект/contract_new.csv') # локально Windiws work
except:
contract_new = pd.read_csv('/datasets/contract_new.csv') # онлайн
contract_new.name = 'Информация о договоре'
# personal_new
try:
personal_new = pd.read_csv('') # онлайн
except:
try:
personal_new = pd.read_csv('D:/YandexDisk/Яндекс практикум/18 Финальный проект/personal_new.csv') # локально Windiws work
except:
personal_new = pd.read_csv('/datasets/personal_new.csv') # онлайн
personal_new.name = 'Персональные данные клиента'
# internet_new
try:
internet_new = pd.read_csv('') # онлайн
except:
try:
internet_new = pd.read_csv('D:/YandexDisk/Яндекс практикум/18 Финальный проект/internet_new.csv') # локально Windiws work
except:
internet_new = pd.read_csv('/datasets/internet_new.csv') # онлайн
internet_new.name = 'Информация об интернет-услугах'
# phone_new
try:
phone_new = pd.read_csv('https://code.s3.yandex.net/datasets/phone_new.csv') # онлайн
except:
try:
phone_new = pd.read_csv('D:/YandexDisk/Яндекс практикум/18 Финальный проект/phone_new.csv') # локально Windiws work
except:
phone_new = pd.read_csv('/datasets/phone_new.csv') # онлайн
phone_new.name = 'Информация об услугах телефонии'
Изучим таблицы. Для этого напишем функцию для вывода информации о датафреймах.
def data_analysis (data):
print ('\033[1m' + data.name +'\033[0m')
display(data.head()),
print()
print('Количество строк:', data.shape[0])
print('Количество столбцов:', data.shape[1])
print()
print('Информация о таблице:')
data.info()
print()
print()
data_list = [contract_new, personal_new, internet_new, phone_new]
for i in data_list:
data_analysis(i)
Информация о договоре
| customerID | BeginDate | EndDate | Type | PaperlessBilling | PaymentMethod | MonthlyCharges | TotalCharges | |
|---|---|---|---|---|---|---|---|---|
| 0 | 7590-VHVEG | 2020-01-01 | No | Month-to-month | Yes | Electronic check | 29.85 | 31.04 |
| 1 | 5575-GNVDE | 2017-04-01 | No | One year | No | Mailed check | 56.95 | 2071.84 |
| 2 | 3668-QPYBK | 2019-10-01 | No | Month-to-month | Yes | Mailed check | 53.85 | 226.17 |
| 3 | 7795-CFOCW | 2016-05-01 | No | One year | No | Bank transfer (automatic) | 42.30 | 1960.6 |
| 4 | 9237-HQITU | 2019-09-01 | No | Month-to-month | Yes | Electronic check | 70.70 | 353.5 |
Количество строк: 7043
Количество столбцов: 8
Информация о таблице:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 7043 entries, 0 to 7042
Data columns (total 8 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 customerID 7043 non-null object
1 BeginDate 7043 non-null object
2 EndDate 7043 non-null object
3 Type 7043 non-null object
4 PaperlessBilling 7043 non-null object
5 PaymentMethod 7043 non-null object
6 MonthlyCharges 7043 non-null float64
7 TotalCharges 7043 non-null object
dtypes: float64(1), object(7)
memory usage: 440.3+ KB
Персональные данные клиента
| customerID | gender | SeniorCitizen | Partner | Dependents | |
|---|---|---|---|---|---|
| 0 | 7590-VHVEG | Female | 0 | Yes | No |
| 1 | 5575-GNVDE | Male | 0 | No | No |
| 2 | 3668-QPYBK | Male | 0 | No | No |
| 3 | 7795-CFOCW | Male | 0 | No | No |
| 4 | 9237-HQITU | Female | 0 | No | No |
Количество строк: 7043
Количество столбцов: 5
Информация о таблице:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 7043 entries, 0 to 7042
Data columns (total 5 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 customerID 7043 non-null object
1 gender 7043 non-null object
2 SeniorCitizen 7043 non-null int64
3 Partner 7043 non-null object
4 Dependents 7043 non-null object
dtypes: int64(1), object(4)
memory usage: 275.2+ KB
Информация об интернет-услугах
| customerID | InternetService | OnlineSecurity | OnlineBackup | DeviceProtection | TechSupport | StreamingTV | StreamingMovies | |
|---|---|---|---|---|---|---|---|---|
| 0 | 7590-VHVEG | DSL | No | Yes | No | No | No | No |
| 1 | 5575-GNVDE | DSL | Yes | No | Yes | No | No | No |
| 2 | 3668-QPYBK | DSL | Yes | Yes | No | No | No | No |
| 3 | 7795-CFOCW | DSL | Yes | No | Yes | Yes | No | No |
| 4 | 9237-HQITU | Fiber optic | No | No | No | No | No | No |
Количество строк: 5517
Количество столбцов: 8
Информация о таблице:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 5517 entries, 0 to 5516
Data columns (total 8 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 customerID 5517 non-null object
1 InternetService 5517 non-null object
2 OnlineSecurity 5517 non-null object
3 OnlineBackup 5517 non-null object
4 DeviceProtection 5517 non-null object
5 TechSupport 5517 non-null object
6 StreamingTV 5517 non-null object
7 StreamingMovies 5517 non-null object
dtypes: object(8)
memory usage: 344.9+ KB
Информация об услугах телефонии
| customerID | MultipleLines | |
|---|---|---|
| 0 | 5575-GNVDE | No |
| 1 | 3668-QPYBK | No |
| 2 | 9237-HQITU | No |
| 3 | 9305-CDSKC | Yes |
| 4 | 1452-KIOVK | Yes |
Количество строк: 6361 Количество столбцов: 2 Информация о таблице: <class 'pandas.core.frame.DataFrame'> RangeIndex: 6361 entries, 0 to 6360 Data columns (total 2 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 customerID 6361 non-null object 1 MultipleLines 6361 non-null object dtypes: object(2) memory usage: 99.5+ KB
Информация в датафреймах представлена количественными и категориальными значениями, встречаются типы данных: float64, object, int64
Датафрейм с информацией о договоре содержит 7043 строк и 8 колонок, датафрейм с информацией о персональных данных клиента - 7043 строк и 5 колонок, датафрейм с информацией об интернет-услугах - 5517 строк и 8 колонок, датафрейм с информацией об услугах телефонии - 6361 строк и 2 колонки.
Название столбцов не соответствует стилю кода языка Python согласно руководству PEP8.
Объединим данные перед проведением детальным анализом данных:
df = contract_new.merge(
personal_new, on='customerID', how='outer'
).merge(
internet_new, on='customerID', how='outer'
).merge(
phone_new, on='customerID', how='outer'
)
df.name = 'Объединенная таблица'
print('Количество строк:', df.shape[0])
print('Количество столбцов:', df.shape[1])
print('Название столбцов:', df.columns.values)
Количество строк: 7043 Количество столбцов: 20 Название столбцов: ['customerID' 'BeginDate' 'EndDate' 'Type' 'PaperlessBilling' 'PaymentMethod' 'MonthlyCharges' 'TotalCharges' 'gender' 'SeniorCitizen' 'Partner' 'Dependents' 'InternetService' 'OnlineSecurity' 'OnlineBackup' 'DeviceProtection' 'TechSupport' 'StreamingTV' 'StreamingMovies' 'MultipleLines']
Объединенная таблица df содержит 7043 строк и 20 колонок, объединение прошло успешно.
# код тимлида для проверки
data_analysis(df)
Объединенная таблица
| customerID | BeginDate | EndDate | Type | PaperlessBilling | PaymentMethod | MonthlyCharges | TotalCharges | gender | SeniorCitizen | Partner | Dependents | InternetService | OnlineSecurity | OnlineBackup | DeviceProtection | TechSupport | StreamingTV | StreamingMovies | MultipleLines | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 7590-VHVEG | 2020-01-01 | No | Month-to-month | Yes | Electronic check | 29.85 | 31.04 | Female | 0 | Yes | No | DSL | No | Yes | No | No | No | No | NaN |
| 1 | 5575-GNVDE | 2017-04-01 | No | One year | No | Mailed check | 56.95 | 2071.84 | Male | 0 | No | No | DSL | Yes | No | Yes | No | No | No | No |
| 2 | 3668-QPYBK | 2019-10-01 | No | Month-to-month | Yes | Mailed check | 53.85 | 226.17 | Male | 0 | No | No | DSL | Yes | Yes | No | No | No | No | No |
| 3 | 7795-CFOCW | 2016-05-01 | No | One year | No | Bank transfer (automatic) | 42.30 | 1960.6 | Male | 0 | No | No | DSL | Yes | No | Yes | Yes | No | No | NaN |
| 4 | 9237-HQITU | 2019-09-01 | No | Month-to-month | Yes | Electronic check | 70.70 | 353.5 | Female | 0 | No | No | Fiber optic | No | No | No | No | No | No | No |
Количество строк: 7043 Количество столбцов: 20 Информация о таблице: <class 'pandas.core.frame.DataFrame'> Int64Index: 7043 entries, 0 to 7042 Data columns (total 20 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 customerID 7043 non-null object 1 BeginDate 7043 non-null object 2 EndDate 7043 non-null object 3 Type 7043 non-null object 4 PaperlessBilling 7043 non-null object 5 PaymentMethod 7043 non-null object 6 MonthlyCharges 7043 non-null float64 7 TotalCharges 7043 non-null object 8 gender 7043 non-null object 9 SeniorCitizen 7043 non-null int64 10 Partner 7043 non-null object 11 Dependents 7043 non-null object 12 InternetService 5517 non-null object 13 OnlineSecurity 5517 non-null object 14 OnlineBackup 5517 non-null object 15 DeviceProtection 5517 non-null object 16 TechSupport 5517 non-null object 17 StreamingTV 5517 non-null object 18 StreamingMovies 5517 non-null object 19 MultipleLines 6361 non-null object dtypes: float64(1), int64(1), object(18) memory usage: 1.1+ MB
Приведем названия столбцов согласно правильного стиля PEP8 , для этого построим функцию:
def convert_to_snake(columns):
return columns.str.replace('(?<=[a-z])(?=[A-Z])', '_', regex=True).str.lower()
df.columns = convert_to_snake(df.columns)
df.columns
Index(['customer_id', 'begin_date', 'end_date', 'type', 'paperless_billing',
'payment_method', 'monthly_charges', 'total_charges', 'gender',
'senior_citizen', 'partner', 'dependents', 'internet_service',
'online_security', 'online_backup', 'device_protection', 'tech_support',
'streaming_tv', 'streaming_movies', 'multiple_lines'],
dtype='object')
Выделим целевой признак. Рассмотрим подробно признак "Дата окончания пользования услугами".
df['end_date'].unique()
array(['No', '2017-05-01', '2016-03-01', '2018-09-01', '2018-11-01',
'2018-12-01', '2019-08-01', '2018-07-01', '2017-09-01',
'2015-09-01', '2016-07-01', '2016-06-01', '2018-03-01',
'2019-02-01', '2018-06-01', '2019-06-01', '2020-01-01',
'2019-11-01', '2016-09-01', '2015-06-01', '2016-12-01',
'2019-05-01', '2019-04-01', '2017-06-01', '2017-08-01',
'2018-04-01', '2018-08-01', '2018-02-01', '2019-07-01',
'2015-12-01', '2014-06-01', '2018-10-01', '2019-01-01',
'2017-07-01', '2017-12-01', '2018-05-01', '2015-11-01',
'2019-10-01', '2019-03-01', '2016-02-01', '2016-10-01',
'2018-01-01', '2017-11-01', '2015-10-01', '2019-12-01',
'2015-07-01', '2017-04-01', '2015-02-01', '2017-03-01',
'2016-05-01', '2016-11-01', '2015-08-01', '2019-09-01',
'2017-10-01', '2017-02-01', '2016-08-01', '2016-04-01',
'2015-05-01', '2014-09-01', '2014-10-01', '2017-01-01',
'2015-03-01', '2015-01-01', '2016-01-01', '2015-04-01',
'2014-12-01', '2014-11-01'], dtype=object)
Рассматривая данный признак можно выделить, что факт ухода клиента оператора связи «Ниединогоразрыва.ком» подтверждается наличем даты уходы. Если факт ухода не зафиксирован, значением колонки 'EndDate' будет 'No'. Создадим целевой признак, при котором значение 1 будет соответсовать факту ухода клиента, 0 - факту, что клиент продолжается пользоваться услугами оператора.
df['leave'] = (
df['end_date'].where(
df['end_date'] == 'No', 'Yes')
)
display(df[df['end_date'] != 'No'][['begin_date','end_date','leave']].head())
df[df['end_date'] == 'No'][['begin_date','end_date','leave']].head()
| begin_date | end_date | leave | |
|---|---|---|---|
| 9 | 2014-12-01 | 2017-05-01 | Yes |
| 15 | 2014-05-01 | 2016-03-01 | Yes |
| 25 | 2017-08-01 | 2018-09-01 | Yes |
| 30 | 2014-03-01 | 2018-11-01 | Yes |
| 35 | 2014-02-01 | 2018-12-01 | Yes |
| begin_date | end_date | leave | |
|---|---|---|---|
| 0 | 2020-01-01 | No | No |
| 1 | 2017-04-01 | No | No |
| 2 | 2019-10-01 | No | No |
| 3 | 2016-05-01 | No | No |
| 4 | 2019-09-01 | No | No |
Корректность созданнного признака подтверждена. Проверим дисбаланс классов:
print ('Процентное соотношение значений столбца "leave":\n',
round(df['leave'].value_counts()/df['leave'].count()*100))
print ()
(df['leave'].value_counts()/df['leave'].count()*100).plot(
kind='bar',
grid = True
).set(
ylabel = 'Относительное количество, %',
xlabel = 'Факт ухода клиента: \n "No" - Клиент не ушел, "Yes" - Клиент ушел',
title = 'Распределение оставшихся и ушедщих клиентов'
)
plt.show()
Процентное соотношение значений столбца "leave": No 84.0 Yes 16.0 Name: leave, dtype: float64
Таким образом, количество клиентов, ушедщих от оператора связи «Ниединогоразрыва.ком» составляет 16 % против 84% неушедщих. Дисбаланс классов должен быть учтен в дальнейшем при обучении модели машинного обучения для задачи классификации.
# код тимлида для проверки
df['leave'].value_counts(normalize=True)
No 0.843675 Yes 0.156325 Name: leave, dtype: float64
Проведем анализ данных для объединенной таблицы:
pd.set_option('display.max_columns', None)
data_analysis(df)
print()
print('Количество дупликатов:', df['customer_id'].duplicated().sum())
print('Количество пропусков:', df.isna().sum().sum())
print('Количество строк с пропусками:', df[df.isna().sum(axis= 1) > 0].shape[0])
print()
Объединенная таблица
| customer_id | begin_date | end_date | type | paperless_billing | payment_method | monthly_charges | total_charges | gender | senior_citizen | partner | dependents | internet_service | online_security | online_backup | device_protection | tech_support | streaming_tv | streaming_movies | multiple_lines | leave | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 7590-VHVEG | 2020-01-01 | No | Month-to-month | Yes | Electronic check | 29.85 | 31.04 | Female | 0 | Yes | No | DSL | No | Yes | No | No | No | No | NaN | No |
| 1 | 5575-GNVDE | 2017-04-01 | No | One year | No | Mailed check | 56.95 | 2071.84 | Male | 0 | No | No | DSL | Yes | No | Yes | No | No | No | No | No |
| 2 | 3668-QPYBK | 2019-10-01 | No | Month-to-month | Yes | Mailed check | 53.85 | 226.17 | Male | 0 | No | No | DSL | Yes | Yes | No | No | No | No | No | No |
| 3 | 7795-CFOCW | 2016-05-01 | No | One year | No | Bank transfer (automatic) | 42.30 | 1960.6 | Male | 0 | No | No | DSL | Yes | No | Yes | Yes | No | No | NaN | No |
| 4 | 9237-HQITU | 2019-09-01 | No | Month-to-month | Yes | Electronic check | 70.70 | 353.5 | Female | 0 | No | No | Fiber optic | No | No | No | No | No | No | No | No |
Количество строк: 7043 Количество столбцов: 21 Информация о таблице: <class 'pandas.core.frame.DataFrame'> Int64Index: 7043 entries, 0 to 7042 Data columns (total 21 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 customer_id 7043 non-null object 1 begin_date 7043 non-null object 2 end_date 7043 non-null object 3 type 7043 non-null object 4 paperless_billing 7043 non-null object 5 payment_method 7043 non-null object 6 monthly_charges 7043 non-null float64 7 total_charges 7043 non-null object 8 gender 7043 non-null object 9 senior_citizen 7043 non-null int64 10 partner 7043 non-null object 11 dependents 7043 non-null object 12 internet_service 5517 non-null object 13 online_security 5517 non-null object 14 online_backup 5517 non-null object 15 device_protection 5517 non-null object 16 tech_support 5517 non-null object 17 streaming_tv 5517 non-null object 18 streaming_movies 5517 non-null object 19 multiple_lines 6361 non-null object 20 leave 7043 non-null object dtypes: float64(1), int64(1), object(19) memory usage: 1.2+ MB Количество дупликатов: 0 Количество пропусков: 11364 Количество строк с пропусками: 2208
Объединенный датафрейм содержит 21 столбцов, включая созданный целевой признак. Дупликатов по 'customer_id' в датафрейме не обнаружено. Количество пропусков - 11364 шт, при этом количество строк, содержащиие пропуски, равно 2208.
Пропуски содержатся в колонках : 'internet_service', 'online_security', 'online_backup', 'device_protection', 'tech_support', 'streaming_tv', 'streaming_movies', 'multiple_lines', что объясняется неиспользованием клиентами определенных услуг. Пропуски образовались в процессе объединения таблиц.
Столбец 'total_charges' представлена типом данных object, хотя он содержит количественные переменные. Необходимо перевести в тип float64.
Столбец 'senior_citizen' представлена типом данных int64, хотя он содержит категориальные переменные. Необходимо перевести в тип object, при чем перевести значения из [0, 1] в ['No', 'Yes']
Столбцы 'begin_date', 'end_date' представленs типом данных object, хотя они содержат даты. Необходимо перевести в тип datetime64.
Изменим тип данных столбца 'senior_citizen':
print('Количество пенсионеров до преобразования:', df[df['senior_citizen'] == 1]['senior_citizen'].count(), ', Тип данных:', df['senior_citizen'].dtype)
df['senior_citizen'] = (
df['senior_citizen'].mask(
df['senior_citizen'] == 0, 'No').where(
df['senior_citizen'] == 0, 'Yes')
)
print('Количество пенсионеров после преобразования:', df[df['senior_citizen'] == 'Yes']['senior_citizen'].count(), ', Тип данных:', df['senior_citizen'].dtype)
Количество пенсионеров до преобразования: 1142 , Тип данных: int64 Количество пенсионеров после преобразования: 1142 , Тип данных: object
Изменим тип данных столбца 'total_charges':
print('Тип данных столбца "total_charges" до преобразования:', df['total_charges'].dtype)
df['total_charges'] = pd.to_numeric(df['total_charges'],errors='coerce')
print('Тип данных столбца "total_charges" после преобразования:', df['total_charges'].dtype)
Тип данных столбца "total_charges" до преобразования: object Тип данных столбца "total_charges" после преобразования: float64
Изменим тип данных столбов 'begin_date', 'end_date':
print('Тип данных столбцов до преобразования:')
print('begin_date -', df['begin_date'].dtype)
print('end_date -', df['end_date'].dtype)
df['begin_date'] = pd.to_datetime(df['begin_date'], errors='ignore')
df['end_date'] = pd.to_datetime(df['end_date'], errors='coerce')
print('Тип данных столбцов после преобразования:')
print('begin_date -', df['begin_date'].dtype)
print('end_date -', df['end_date'].dtype)
Тип данных столбцов до преобразования: begin_date - object end_date - object Тип данных столбцов после преобразования: begin_date - datetime64[ns] end_date - datetime64[ns]
Изучим пропуски в столбце 'total_charges':
df[df['total_charges'].isna()== True].sample(5)
| customer_id | begin_date | end_date | type | paperless_billing | payment_method | monthly_charges | total_charges | gender | senior_citizen | partner | dependents | internet_service | online_security | online_backup | device_protection | tech_support | streaming_tv | streaming_movies | multiple_lines | leave | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 936 | 5709-LVOEQ | 2020-02-01 | NaT | Two year | No | Mailed check | 80.85 | NaN | Female | No | Yes | Yes | DSL | Yes | Yes | Yes | No | Yes | Yes | No | No |
| 1340 | 1371-DWPAZ | 2020-02-01 | NaT | Two year | No | Credit card (automatic) | 56.05 | NaN | Female | No | Yes | Yes | DSL | Yes | Yes | Yes | Yes | Yes | No | NaN | No |
| 753 | 3115-CZMZD | 2020-02-01 | NaT | Two year | No | Mailed check | 20.25 | NaN | Male | No | No | Yes | NaN | NaN | NaN | NaN | NaN | NaN | NaN | No | No |
| 4380 | 2520-SGTTA | 2020-02-01 | NaT | Two year | No | Mailed check | 20.00 | NaN | Female | No | Yes | Yes | NaN | NaN | NaN | NaN | NaN | NaN | NaN | No | No |
| 1082 | 4367-NUYAO | 2020-02-01 | NaT | Two year | No | Mailed check | 25.75 | NaN | Male | No | Yes | Yes | NaN | NaN | NaN | NaN | NaN | NaN | NaN | Yes | No |
Пропуски в столбце 'total_charges' связаны с тем, что день начала контракта связан с днем выгрузки датасета ( 1 февраля 2020 года), а значит клиенты еще не оплачивали услуги телефонии или интернета. ЗХаменим пропущенные значения на '0':
print('Количество пропусков до заполнения:', df.total_charges.isna().sum())
df['total_charges'] = df['total_charges'].fillna(0)
print('Количество пропусков после заполнения:', df.total_charges.isna().sum())
Количество пропусков до заполнения: 11 Количество пропусков после заполнения: 0
Столбец 'end_date' содержит пропусков после преобразования значений в тип данных 'datetime64'. Заполним пропущенные значения временем выгрузки датасетов, равному 1 февраля 2020 года. Данное заполнение будет использоваться в дальнейшем при создании синтетических признаков.
print('Количество пропусков до заполнения:', df.end_date.isna().sum())
df['end_date'] = df['end_date'].fillna('2020-02-01')
print('Количество пропусков после заполнения:', df.end_date.isna().sum())
Количество пропусков до заполнения: 5942 Количество пропусков после заполнения: 0
Столбец 'multiple_lines' содержит информацию о возможности подключения телефонного аппарата к нескольким линиям одновременно. Пропуски в столбце образовались в процессе объединения таблиц и сигнализируют о том, что клиент не пользуется услугами телефонной связи. Поэтому заменим все пропски на 'No service'.
print('Заполненных строк:', df.multiple_lines.isna().sum())
print()
print ('\033[1m' + 'До заполнения:' +'\033[0m')
print('Уникальные значения:', df['multiple_lines'].unique())
print('Количество строк с пропусками:', df[df.isna().sum(axis= 1) > 0].shape[0])
df['multiple_lines'] = df['multiple_lines'].fillna('No service')
print()
print ('\033[1m' + 'После заполнения:' +'\033[0m')
print('Уникальные значения:', df['multiple_lines'].unique())
print('Количество строк с пропусками:', df[df.isna().sum(axis= 1) > 0].shape[0])
Заполненных строк: 682 До заполнения: Уникальные значения: [nan 'No' 'Yes'] Количество строк с пропусками: 2208 После заполнения: Уникальные значения: ['No service' 'No' 'Yes'] Количество строк с пропусками: 1526
Рассматривая столбцы 'internet_service', 'online_security', 'online_backup', 'device_protection', 'tech_support', 'streaming_tv', 'streaming_movies' видно, что они имеют одинаковое количество пропусков. Пропуски в столбце образовались в процессе объединения таблиц и сигнализируют о том, что клиент не пользуется интернет-услугами от оператора. Заменим пропуски в столбце 'internet_service' заглушкой 'No service', а в столбцах 'online_security', 'online_backup', 'device_protection', 'tech_support', 'streaming_tv', 'streaming_movies' - 'No', при условии что в столбце 'internet_service' присутствуют пропуски:
print('Заполненных строк:', df.internet_service.isna().sum())
print()
print ('\033[1m' + 'До заполнения:' +'\033[0m')
print('Количество строк с пропусками:', df[df.isna().sum(axis= 1) > 0].shape[0])
print('Уникальные значения:')
for i in ['internet_service', 'online_security', 'online_backup',
'device_protection', 'tech_support', 'streaming_tv', 'streaming_movies']:
print(i,'-', df[i].unique())
# Заполнение 'online_security', 'online_backup', 'device_protection', 'tech_support', 'streaming_tv', 'streaming_movies'
# при условии, что в 'internet_service' есть пропуски :
df.loc[df['internet_service'].isna() == True,
['online_security','online_backup', 'device_protection', 'tech_support', 'streaming_tv', 'streaming_movies']
] = df.loc[df['internet_service'].isna() == True,
['online_security','online_backup', 'device_protection', 'tech_support', 'streaming_tv', 'streaming_movies']
].fillna('No')
# Заполнение 'internet_service':
df['internet_service'] = df['internet_service'].fillna('No service')
print()
print ('\033[1m' + 'После заполнения:' +'\033[0m')
print('Количество строк с пропусками:', df[df.isna().sum(axis= 1) > 0].shape[0])
print('Уникальные значения:')
for i in ['internet_service', 'online_security', 'online_backup',
'device_protection', 'tech_support', 'streaming_tv', 'streaming_movies']:
print(i,'-', df[i].unique())
Заполненных строк: 1526 До заполнения: Количество строк с пропусками: 1526 Уникальные значения: internet_service - ['DSL' 'Fiber optic' nan] online_security - ['No' 'Yes' nan] online_backup - ['Yes' 'No' nan] device_protection - ['No' 'Yes' nan] tech_support - ['No' 'Yes' nan] streaming_tv - ['No' 'Yes' nan] streaming_movies - ['No' 'Yes' nan] После заполнения: Количество строк с пропусками: 0 Уникальные значения: internet_service - ['DSL' 'Fiber optic' 'No service'] online_security - ['No' 'Yes'] online_backup - ['Yes' 'No'] device_protection - ['No' 'Yes'] tech_support - ['No' 'Yes'] streaming_tv - ['No' 'Yes'] streaming_movies - ['No' 'Yes']
Проверим обработанный датасет:
data_analysis(df)
Объединенная таблица
| customer_id | begin_date | end_date | type | paperless_billing | payment_method | monthly_charges | total_charges | gender | senior_citizen | partner | dependents | internet_service | online_security | online_backup | device_protection | tech_support | streaming_tv | streaming_movies | multiple_lines | leave | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 7590-VHVEG | 2020-01-01 | 2020-02-01 | Month-to-month | Yes | Electronic check | 29.85 | 31.04 | Female | No | Yes | No | DSL | No | Yes | No | No | No | No | No service | No |
| 1 | 5575-GNVDE | 2017-04-01 | 2020-02-01 | One year | No | Mailed check | 56.95 | 2071.84 | Male | No | No | No | DSL | Yes | No | Yes | No | No | No | No | No |
| 2 | 3668-QPYBK | 2019-10-01 | 2020-02-01 | Month-to-month | Yes | Mailed check | 53.85 | 226.17 | Male | No | No | No | DSL | Yes | Yes | No | No | No | No | No | No |
| 3 | 7795-CFOCW | 2016-05-01 | 2020-02-01 | One year | No | Bank transfer (automatic) | 42.30 | 1960.60 | Male | No | No | No | DSL | Yes | No | Yes | Yes | No | No | No service | No |
| 4 | 9237-HQITU | 2019-09-01 | 2020-02-01 | Month-to-month | Yes | Electronic check | 70.70 | 353.50 | Female | No | No | No | Fiber optic | No | No | No | No | No | No | No | No |
Количество строк: 7043 Количество столбцов: 21 Информация о таблице: <class 'pandas.core.frame.DataFrame'> Int64Index: 7043 entries, 0 to 7042 Data columns (total 21 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 customer_id 7043 non-null object 1 begin_date 7043 non-null datetime64[ns] 2 end_date 7043 non-null datetime64[ns] 3 type 7043 non-null object 4 paperless_billing 7043 non-null object 5 payment_method 7043 non-null object 6 monthly_charges 7043 non-null float64 7 total_charges 7043 non-null float64 8 gender 7043 non-null object 9 senior_citizen 7043 non-null object 10 partner 7043 non-null object 11 dependents 7043 non-null object 12 internet_service 7043 non-null object 13 online_security 7043 non-null object 14 online_backup 7043 non-null object 15 device_protection 7043 non-null object 16 tech_support 7043 non-null object 17 streaming_tv 7043 non-null object 18 streaming_movies 7043 non-null object 19 multiple_lines 7043 non-null object 20 leave 7043 non-null object dtypes: datetime64[ns](2), float64(2), object(17) memory usage: 1.2+ MB
Таким образом, пропуски в датасете были обработаны путем замены на соответсвующие значения. В столбце 'multiple_lines' было заменно 682 пропуска путем замены на строковое значение 'No', в столбцах 'internet_service', 'online_security', 'online_backup', 'device_protection', 'tech_support', 'streaming_tv', 'streaming_movies' было заменно по 1526 пропусков, при этом в столбце 'internet_service' путем замены на строковое значение 'No internet', в остальных - на значение 'No'.
Проведем описательную статистику для столбцов с количественными значениями 'monthly_charges','total_charges'. Для этого построим функцию анализа признаков с количественными значениями, построения графиков плотности распределения и диаграмм размаха.
def analysis_quantitatives(features_list):
for i in features_list:
print()
print('\033[1m' + i,':' +'\033[0m')
print()
print('Клиенты ушли от оператора:')
print(df[i][df['leave'] == 'Yes'].describe())
print()
print('Клиенты не ушли от оператора:')
print(df[i][df['leave'] == 'No'].describe())
sns.set()
lines, axes = plt.subplots(1, 2, figsize=(16, 4))
sns.histplot(data=df, hue='leave', x=i, ax=axes[0], kde=True).set_title(i + ". Плотность распределения", fontsize=18)
sns.boxplot(data=df, x=i, y='leave', ax=axes[1]).set_title(i + ". Диаграмма размаха", fontsize=18)
plt.show()
analysis_quantitatives(['monthly_charges','total_charges'])
monthly_charges :
Клиенты ушли от оператора:
count 1101.000000
mean 75.546004
std 29.116213
min 18.400000
25% 56.150000
50% 84.200000
75% 99.500000
max 118.750000
Name: monthly_charges, dtype: float64
Клиенты не ушли от оператора:
count 5942.000000
mean 62.763455
std 29.844462
min 18.250000
25% 30.062500
50% 69.200000
75% 87.237500
max 118.600000
Name: monthly_charges, dtype: float64
total_charges :
Клиенты ушли от оператора:
count 1101.000000
mean 2371.377275
std 1581.862275
min 77.840000
25% 1048.050000
50% 2139.030000
75% 3404.910000
max 7649.760000
Name: total_charges, dtype: float64
Клиенты не ушли от оператора:
count 5942.000000
mean 2067.866420
std 2193.898483
min 0.000000
25% 374.352500
50% 1192.800000
75% 3173.837500
max 9221.380000
Name: total_charges, dtype: float64
Рассматривая распределения признаков в разрезе целевой переменной, можно отметить:
Выведем случайным образом строки датасета с значением 'total_charges' выше 7500:
df[df['total_charges']>7500].sample(10)
| customer_id | begin_date | end_date | type | paperless_billing | payment_method | monthly_charges | total_charges | gender | senior_citizen | partner | dependents | internet_service | online_security | online_backup | device_protection | tech_support | streaming_tv | streaming_movies | multiple_lines | leave | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2689 | 8628-MFKAX | 2014-02-01 | 2020-02-01 | Two year | Yes | Credit card (automatic) | 116.75 | 8910.36 | Female | Yes | Yes | No | Fiber optic | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No |
| 2515 | 8869-LIHMK | 2014-10-01 | 2020-02-01 | Two year | Yes | Bank transfer (automatic) | 115.10 | 8029.38 | Female | No | No | No | Fiber optic | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No |
| 5995 | 2193-SFWQW | 2014-02-01 | 2020-02-01 | Two year | No | Bank transfer (automatic) | 111.95 | 8060.40 | Male | No | Yes | Yes | Fiber optic | Yes | Yes | Yes | Yes | Yes | Yes | No | No |
| 3348 | 2172-EJXVF | 2014-03-01 | 2020-02-01 | One year | Yes | Electronic check | 105.90 | 7744.47 | Female | Yes | No | No | Fiber optic | No | Yes | Yes | No | Yes | Yes | Yes | No |
| 1280 | 2388-LAESQ | 2014-02-01 | 2020-02-01 | Two year | Yes | Bank transfer (automatic) | 114.85 | 8765.35 | Female | Yes | Yes | No | Fiber optic | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No |
| 6035 | 9835-ZIITK | 2014-06-01 | 2020-01-01 | One year | Yes | Electronic check | 110.85 | 7649.76 | Male | Yes | Yes | No | Fiber optic | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| 57 | 5067-XJQFU | 2014-08-01 | 2020-02-01 | One year | Yes | Electronic check | 108.45 | 7730.32 | Male | Yes | Yes | Yes | Fiber optic | No | Yes | Yes | Yes | Yes | Yes | Yes | No |
| 3964 | 2632-IVXVF | 2014-06-01 | 2020-02-01 | Two year | No | Credit card (automatic) | 111.75 | 7902.96 | Female | No | Yes | Yes | Fiber optic | Yes | No | Yes | Yes | Yes | Yes | Yes | No |
| 6225 | 1452-UZOSF | 2014-02-01 | 2020-02-01 | Two year | Yes | Credit card (automatic) | 106.10 | 7639.20 | Male | No | Yes | Yes | Fiber optic | Yes | Yes | Yes | Yes | Yes | No | Yes | No |
| 4031 | 8309-PPCED | 2014-02-01 | 2020-02-01 | Two year | Yes | Bank transfer (automatic) | 110.45 | 8429.54 | Female | No | Yes | No | Fiber optic | No | Yes | Yes | Yes | Yes | Yes | Yes | No |
Из приведенных строк видно, что данные не являются выбросами, так как они объясняются большим количестовом дополнительных услуг, которыми пользуются клиенты.
Рассмотрим распределение признаков в датасете:
col_list = ['type', 'paperless_billing', 'gender', 'senior_citizen',
'partner', 'dependents', 'payment_method', 'internet_service', 'online_security', 'online_backup',
'device_protection', 'tech_support', 'streaming_tv', 'streaming_movies', 'multiple_lines']
name_list = ['Тип договора', 'Выставления счёта по электронной почте', 'Пол клиента', 'Пенсионный статус',
'Наличие супруга/супруги', 'Наличие иждивенцев', 'Способ оплаты', 'Наличие услуг Интернет', 'Межсетевой экран',
'Облачное хранилище файлов', 'Антивирус', 'Выделенная линия технической поддержки',
'Онлайн-ТВ', 'Онлайн-кинотеатр', 'Возможность подключения телефонного \n аппарата к нескольким линиям']
pic_box = plt.figure(figsize=(16,20))
for i in range(15):
pic_box.add_subplot(5,3,i+1)
sns.histplot(data=df, x=df[col_list[i]], hue='leave', multiple='dodge', shrink = 0.8)
plt.title(str(i+1)+'.'+ name_list[i], fontsize=14)
plt.tight_layout()
Рассмотрим распределение признаков в процентном отношении:
for i in range(15):
plt.figure(figsize=(12,4))
for b in df[col_list[i]].unique():
plt.subplot(1,len(df[col_list[i]].unique()),list(df[col_list[i]].unique()).index(b)+1)
sns.histplot(data=df[df[col_list[i]] == b], x='leave', hue ='leave',
shrink = 0.8, multiple='dodge', stat = 'percent', bins = 4)
plt.title(str(i+1)+'.'+ name_list[i] + '\n- ' + b, fontsize=12)
plt.tight_layout()
Рассматривая распределения признаков в разрезе целевой переменной, можно отметить:
Из известных данных создадим следующие синтетические признаки:
Из известных данных создадим синтетический признак "Длительность обслуживания клиента" - 'duration':
# Длительность обслуживания клиента 'duration'
df['duration'] = (df['end_date'] - df['begin_date'])/np.timedelta64(1,'D')
Создадим синтетический признак "Близость окончания договора" - 'contract_end'. Для каждого клиента будет сгенерировано значение в зависимости от типа его договора с оператором связи. Вставленное значение будет варьироваться от 0 до 1, где значение около 0 - это договор только заключен, около 1 - договор подходит к концу и требуется пролонгация.
Создадим функцию для генерирования синтетического признака 'contract_end' и примерим его к датасету:
def contract_end(data):
if data['type'] == 'Month-to-month':
return (data['end_date'] - data['begin_date'])/np.timedelta64(1,'M')\
- np.floor((data['end_date'] - data['begin_date'])/np.timedelta64(1,'M'))
if data['type'] == 'One year':
return (data['end_date'] - data['begin_date'])/np.timedelta64(1,'Y')\
- np.floor((data['end_date'] - data['begin_date'])/np.timedelta64(1,'Y'))
if data['type'] == 'Two year':
return (data['end_date'] - data['begin_date'])/np.timedelta64(2,'Y')\
- np.floor((data['end_date'] - data['begin_date'])/np.timedelta64(2,'Y'))
df['contract_end'] = df.apply(contract_end, axis = 1)
Проведем описательную статистику для синтетических признаков, построим график плотности распределения и диаграмму размаха.
analysis_quantitatives(['duration', 'contract_end'])
duration :
Клиенты ушли от оператора:
count 1101.000000
mean 924.863760
std 458.771309
min 28.000000
25% 577.000000
50% 915.000000
75% 1249.000000
max 2129.000000
Name: duration, dtype: float64
Клиенты не ушли от оператора:
count 5942.000000
mean 893.681084
std 716.958551
min 0.000000
25% 245.000000
50% 702.000000
75% 1523.000000
max 2314.000000
Name: duration, dtype: float64
contract_end :
Клиенты ушли от оператора:
count 1101.000000
mean 0.483135
std 0.364089
min 0.000041
25% 0.085621
50% 0.457935
75% 0.835767
max 0.999418
Name: contract_end, dtype: float64
Клиенты не ушли от оператора:
count 5942.000000
mean 0.310056
std 0.367864
min 0.000000
25% 0.033183
50% 0.063800
75% 0.625653
max 0.999418
Name: contract_end, dtype: float64
Рассматривая распределения значений в синтетических признаках в разрезе целевой переменной, можно отметить:
Признаки, содержащие временные значения и идентификационную информацию, являются избыточными при построении модели машинного обучения для задачи классификации. Поэтому удалим признаки 'customer_id', 'begin_date', 'end_date'.
df_before_delete = df.copy()
df.drop(['customer_id', 'begin_date', 'end_date'], axis=1, inplace = True)
Проверим обработанный датасет:
data_analysis(df)
Объединенная таблица
| type | paperless_billing | payment_method | monthly_charges | total_charges | gender | senior_citizen | partner | dependents | internet_service | online_security | online_backup | device_protection | tech_support | streaming_tv | streaming_movies | multiple_lines | leave | duration | contract_end | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Month-to-month | Yes | Electronic check | 29.85 | 31.04 | Female | No | Yes | No | DSL | No | Yes | No | No | No | No | No service | No | 31.0 | 0.018501 |
| 1 | One year | No | Mailed check | 56.95 | 2071.84 | Male | No | No | No | DSL | Yes | No | Yes | No | No | No | No | No | 1036.0 | 0.836472 |
| 2 | Month-to-month | Yes | Mailed check | 53.85 | 226.17 | Male | No | No | No | DSL | Yes | Yes | No | No | No | No | No | No | 123.0 | 0.041151 |
| 3 | One year | No | Bank transfer (automatic) | 42.30 | 1960.60 | Male | No | No | No | DSL | Yes | No | Yes | Yes | No | No | No service | No | 1371.0 | 0.753671 |
| 4 | Month-to-month | Yes | Electronic check | 70.70 | 353.50 | Female | No | No | No | Fiber optic | No | No | No | No | No | No | No | No | 153.0 | 0.026797 |
Количество строк: 7043 Количество столбцов: 20 Информация о таблице: <class 'pandas.core.frame.DataFrame'> Int64Index: 7043 entries, 0 to 7042 Data columns (total 20 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 type 7043 non-null object 1 paperless_billing 7043 non-null object 2 payment_method 7043 non-null object 3 monthly_charges 7043 non-null float64 4 total_charges 7043 non-null float64 5 gender 7043 non-null object 6 senior_citizen 7043 non-null object 7 partner 7043 non-null object 8 dependents 7043 non-null object 9 internet_service 7043 non-null object 10 online_security 7043 non-null object 11 online_backup 7043 non-null object 12 device_protection 7043 non-null object 13 tech_support 7043 non-null object 14 streaming_tv 7043 non-null object 15 streaming_movies 7043 non-null object 16 multiple_lines 7043 non-null object 17 leave 7043 non-null object 18 duration 7043 non-null float64 19 contract_end 7043 non-null float64 dtypes: float64(4), object(16) memory usage: 1.1+ MB
Таким образом, успешно удалено 3 стобца 'customer_id', 'begin_date', 'end_date' и сгенерированы 2 сентитических признака 'duration', 'contract_end'. Датасет содержит 19 признаков и 7043 объекта, пропущенных значений нет.
Проверку корреляций проведем с помощью библиотеки phik. Построим матрицу корреляции признаков:
phik_overview = df[['leave'] + [x for x in df.columns if x != 'leave']
].phik_matrix(interval_cols=['monthly_charges','total_charges', 'duration', 'contract_end'])
phik_overview.sort_values(by='leave', ascending=False).style.background_gradient(cmap = 'coolwarm')
| leave | type | paperless_billing | payment_method | monthly_charges | total_charges | gender | senior_citizen | partner | dependents | internet_service | online_security | online_backup | device_protection | tech_support | streaming_tv | streaming_movies | multiple_lines | duration | contract_end | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| leave | 1.000000 | 0.094015 | 0.083398 | 0.214832 | 0.226280 | 0.302890 | 0.008581 | 0.086159 | 0.226688 | 0.046871 | 0.056621 | 0.132594 | 0.229482 | 0.218380 | 0.103652 | 0.200198 | 0.222232 | 0.105101 | 0.374569 | 0.318624 |
| duration | 0.374569 | 0.634155 | 0.026799 | 0.350964 | 0.387727 | 0.848337 | 0.000000 | 0.063315 | 0.453688 | 0.198729 | 0.060845 | 0.395693 | 0.414982 | 0.426931 | 0.404795 | 0.339313 | 0.339313 | 0.347289 | 1.000000 | 0.730764 |
| contract_end | 0.318624 | 0.742251 | 0.184774 | 0.295072 | 0.302197 | 0.459746 | 0.000000 | 0.148757 | 0.326143 | 0.252733 | 0.262514 | 0.300852 | 0.215581 | 0.280061 | 0.336217 | 0.188384 | 0.188738 | 0.184148 | 0.730764 | 1.000000 |
| total_charges | 0.302890 | 0.470860 | 0.201703 | 0.335666 | 0.710905 | 1.000000 | 0.000000 | 0.135650 | 0.381958 | 0.084247 | 0.490081 | 0.522090 | 0.622445 | 0.640977 | 0.550065 | 0.641488 | 0.643210 | 0.467787 | 0.848337 | 0.459746 |
| online_backup | 0.229482 | 0.098884 | 0.196443 | 0.282475 | 0.629541 | 0.622445 | 0.009882 | 0.102065 | 0.219223 | 0.031533 | 0.233602 | 0.430425 | 1.000000 | 0.458211 | 0.445130 | 0.428007 | 0.417170 | 0.140081 | 0.414982 | 0.215581 |
| partner | 0.226688 | 0.179736 | 0.013218 | 0.243008 | 0.203545 | 0.381958 | 0.000000 | 0.016992 | 1.000000 | 0.652122 | 0.000000 | 0.221673 | 0.219223 | 0.238079 | 0.185993 | 0.193258 | 0.182011 | 0.086249 | 0.453688 | 0.326143 |
| monthly_charges | 0.226280 | 0.388444 | 0.467812 | 0.399526 | 1.000000 | 0.710905 | 0.008175 | 0.304985 | 0.203545 | 0.184366 | 0.919002 | 0.551621 | 0.629541 | 0.667481 | 0.576525 | 0.835340 | 0.833307 | 0.709983 | 0.387727 | 0.302197 |
| streaming_movies | 0.222232 | 0.069608 | 0.325551 | 0.378907 | 0.833307 | 0.643210 | 0.000000 | 0.186141 | 0.182011 | 0.058999 | 0.272782 | 0.289097 | 0.417170 | 0.589888 | 0.424078 | 0.742479 | 1.000000 | 0.170432 | 0.339313 | 0.188738 |
| device_protection | 0.218380 | 0.137610 | 0.160796 | 0.306866 | 0.667481 | 0.640977 | 0.000000 | 0.090686 | 0.238079 | 0.010416 | 0.232916 | 0.418474 | 0.458211 | 1.000000 | 0.499267 | 0.575536 | 0.589888 | 0.145710 | 0.426931 | 0.280061 |
| payment_method | 0.214832 | 0.277462 | 0.370495 | 1.000000 | 0.399526 | 0.335666 | 0.000000 | 0.292725 | 0.243008 | 0.224903 | 0.323886 | 0.262911 | 0.282475 | 0.306866 | 0.272101 | 0.377209 | 0.378907 | 0.174849 | 0.350964 | 0.295072 |
| streaming_tv | 0.200198 | 0.066961 | 0.343524 | 0.377209 | 0.835340 | 0.641488 | 0.000000 | 0.163120 | 0.193258 | 0.017331 | 0.272818 | 0.272186 | 0.428007 | 0.575536 | 0.422242 | 1.000000 | 0.742479 | 0.166899 | 0.339313 | 0.188384 |
| online_security | 0.132594 | 0.152145 | 0.000000 | 0.262911 | 0.551621 | 0.522090 | 0.018397 | 0.057028 | 0.221673 | 0.124945 | 0.241421 | 1.000000 | 0.430425 | 0.418474 | 0.528391 | 0.272186 | 0.289097 | 0.095572 | 0.395693 | 0.300852 |
| multiple_lines | 0.105101 | 0.244410 | 0.099953 | 0.174849 | 0.709983 | 0.467787 | 0.000000 | 0.087925 | 0.086249 | 0.011198 | 0.739808 | 0.095572 | 0.140081 | 0.145710 | 0.098571 | 0.166899 | 0.170432 | 1.000000 | 0.347289 | 0.184148 |
| tech_support | 0.103652 | 0.179999 | 0.055929 | 0.272101 | 0.576525 | 0.550065 | 0.000000 | 0.092565 | 0.185993 | 0.096912 | 0.239663 | 0.528391 | 0.445130 | 0.499267 | 1.000000 | 0.422242 | 0.424078 | 0.098571 | 0.404795 | 0.336217 |
| type | 0.094015 | 1.000000 | 0.106860 | 0.277462 | 0.388444 | 0.470860 | 0.000000 | 0.086231 | 0.179736 | 0.147680 | 0.505187 | 0.152145 | 0.098884 | 0.137610 | 0.179999 | 0.066961 | 0.069608 | 0.244410 | 0.634155 | 0.742251 |
| senior_citizen | 0.086159 | 0.086231 | 0.242133 | 0.292725 | 0.304985 | 0.135650 | 0.000000 | 1.000000 | 0.016992 | 0.324576 | 0.160702 | 0.057028 | 0.102065 | 0.090686 | 0.092565 | 0.163120 | 0.186141 | 0.087925 | 0.063315 | 0.148757 |
| paperless_billing | 0.083398 | 0.106860 | 1.000000 | 0.370495 | 0.467812 | 0.201703 | 0.000000 | 0.242133 | 0.013218 | 0.172593 | 0.231438 | 0.000000 | 0.196443 | 0.160796 | 0.055929 | 0.343524 | 0.325551 | 0.099953 | 0.026799 | 0.184774 |
| internet_service | 0.056621 | 0.505187 | 0.231438 | 0.323886 | 0.919002 | 0.490081 | 0.000000 | 0.160702 | 0.000000 | 0.108463 | 1.000000 | 0.241421 | 0.233602 | 0.232916 | 0.239663 | 0.272818 | 0.272782 | 0.739808 | 0.060845 | 0.262514 |
| dependents | 0.046871 | 0.147680 | 0.172593 | 0.224903 | 0.184366 | 0.084247 | 0.000000 | 0.324576 | 0.652122 | 1.000000 | 0.108463 | 0.124945 | 0.031533 | 0.010416 | 0.096912 | 0.017331 | 0.058999 | 0.011198 | 0.198729 | 0.252733 |
| gender | 0.008581 | 0.000000 | 0.000000 | 0.000000 | 0.008175 | 0.000000 | 1.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.018397 | 0.009882 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 |
plot_correlation_matrix(phik_overview.values, x_labels=phik_overview.columns, y_labels=phik_overview.index,
vmin=0, vmax=1, color_map='Blues', title=r'correlation $\phi_K$', fontsize_factor=1.5,
figsize=(16,20))
plt.tight_layout()
Построенная матрица корреляции показывает, что на целевой признак (отток клиентов) сильнее всего влияет: длительность обслуживания клиента (0,37), близость окончания договора (0,32) и количество потраченных денег (0,3). Меньше всего влияют на целевой признак: пол клиента (0,01), наличие иждивенцев (0,05) и пользование интернет-услугами (0,06). При создании моделей машинного обучения рекумендуется не использовать признаки с такой низкой корреляцией. Также при создании линейных моделей необходимо учитывать появление высокой мультиколлинеарности, которая присутствует между признаками 'internet_service' и 'monthly_charges'. Таким образом рекомендуется удалить признаки 'gender', 'dependents' и 'internet_service'.
Признаки, содержащие временные значения и идентификационную информацию, являются избыточными при построении модели машинного обучения для задачи классификации. Поэтому удалим признаки 'customer_id', 'begin_date', 'end_date'.
df.drop(['gender', 'dependents', 'internet_service'], axis=1, inplace = True)
Проверим обработанный датасет:
df.columns
Index(['type', 'paperless_billing', 'payment_method', 'monthly_charges',
'total_charges', 'senior_citizen', 'partner', 'online_security',
'online_backup', 'device_protection', 'tech_support', 'streaming_tv',
'streaming_movies', 'multiple_lines', 'leave', 'duration',
'contract_end'],
dtype='object')
data_analysis(df)
Объединенная таблица
| type | paperless_billing | payment_method | monthly_charges | total_charges | senior_citizen | partner | online_security | online_backup | device_protection | tech_support | streaming_tv | streaming_movies | multiple_lines | leave | duration | contract_end | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Month-to-month | Yes | Electronic check | 29.85 | 31.04 | No | Yes | No | Yes | No | No | No | No | No service | No | 31.0 | 0.018501 |
| 1 | One year | No | Mailed check | 56.95 | 2071.84 | No | No | Yes | No | Yes | No | No | No | No | No | 1036.0 | 0.836472 |
| 2 | Month-to-month | Yes | Mailed check | 53.85 | 226.17 | No | No | Yes | Yes | No | No | No | No | No | No | 123.0 | 0.041151 |
| 3 | One year | No | Bank transfer (automatic) | 42.30 | 1960.60 | No | No | Yes | No | Yes | Yes | No | No | No service | No | 1371.0 | 0.753671 |
| 4 | Month-to-month | Yes | Electronic check | 70.70 | 353.50 | No | No | No | No | No | No | No | No | No | No | 153.0 | 0.026797 |
Количество строк: 7043 Количество столбцов: 17 Информация о таблице: <class 'pandas.core.frame.DataFrame'> Int64Index: 7043 entries, 0 to 7042 Data columns (total 17 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 type 7043 non-null object 1 paperless_billing 7043 non-null object 2 payment_method 7043 non-null object 3 monthly_charges 7043 non-null float64 4 total_charges 7043 non-null float64 5 senior_citizen 7043 non-null object 6 partner 7043 non-null object 7 online_security 7043 non-null object 8 online_backup 7043 non-null object 9 device_protection 7043 non-null object 10 tech_support 7043 non-null object 11 streaming_tv 7043 non-null object 12 streaming_movies 7043 non-null object 13 multiple_lines 7043 non-null object 14 leave 7043 non-null object 15 duration 7043 non-null float64 16 contract_end 7043 non-null float64 dtypes: float64(4), object(13) memory usage: 990.4+ KB
Таким образом, успешно удалено 3 стобца 'gender', 'dependents', 'internet_service'. Итоговый датасет содержит 16 признаков и 7043 объекта, пропущенных значений нет.
В процессе работы над первым разделом проекта:
Поставлены цель и задачи работы;
Были изучены и предварительно проанализирваны исходные таблицы, после чего таблицы были объедененны в одну общую. Итоговая таблица содержала 7043 строк и 20 колонок. Дупликатов по 'customer_id' в датафрейме не обнаружено. Количество пропусков - 11364 шт, при этом количество строк, содержащиие пропуски, равно 2208. Выявлено, что пропущенные значения возникли в процессе объединения таблиц;
Названия столбцов приведены в соотвествии с правильным стилем PEP8. Создан целевой признак 'leave' (факт ухода клиента от оператора связи). Изменены типы даных в столбцах 'total_charges', 'senior_citizen', 'begin_date', 'end_date'. Пропуски обработаны путем замены на соответсвующие значения. В столбце 'total_charges' заменно 11 пропусrjd путем замены на значение '0', в столбце 'multiple_lines' - 682 пропуска на строковое значение 'No'. В столбцах 'internet_service', 'online_security', 'online_backup', 'device_protection', 'tech_support', 'streaming_tv', 'streaming_movies' заменно по 1526 пропусков, при этом в столбце 'internet_service' путем замены на строковое значение 'No internet', в остальных - на значение 'No';
Рассматривая распределения признаков в разрезе целевой переменной, можно отметить:
Были созданы 2 синтетических признака: длительность обслуживания клиента 'duration', близость окончания договора 'contract_end'.
Построенная матрица корреляции с помощью библиотеки phik показывала, что на целевой признак (отток клиентов) сильнее всего влияет: длительность обслуживания клиента (0,37), близость окончания договора (0,32) и количество потраченных денег (0,3). Меньше всего влияют на целевой признак: пол клиента (0,01), наличие иждивенцев (0,05) и пользование интернет-услугами (0,06). Также имеется высокая мультиколлинеарность между признаками 'internet_service' и 'monthly_charges'.
Из итогового датасета были удалены следующие избыточные признаки, признаки, имеющие низкую корреляцию с целевым признаком и признаки с высокой мультиколлинеарностью:
Итоговый датасет содержит 16 признаков и 7043 объекта, пропущенных значений нет.
Определим в качестве целоевого признака столбец 'leave':
target = df['leave']
features = df.drop(['leave'], axis=1)
Перекодируем целевой признак в числовые значения ['0','1']
print('Количество уникальных значений до замены:')
print(target.value_counts())
target = target.replace(['No', 'Yes'], [0, 1])
print()
print('Количество уникальных значений после замены:')
print(target.value_counts())
Количество уникальных значений до замены: No 5942 Yes 1101 Name: leave, dtype: int64 Количество уникальных значений после замены: 0 5942 1 1101 Name: leave, dtype: int64
Разделим признаки на обучающие и тестовые выборки в соотношении 3 к 1:
features_train, features_test, target_train, target_test = train_test_split(
features, target, test_size=0.33, random_state=140823, stratify=target, shuffle = True)
print('"features_train":', features_train.shape[0],'объектов,',
100 - round((features.shape[0] - features_train.shape[0]) * 100 / features.shape[0]), "%" )
print('"target_train":', target_train.shape[0],'объектов,',
100 - round((target.shape[0] - target_train.shape[0]) * 100 / target.shape[0]), "%" )
print('"features_test":', features_test.shape[0],'объектов,',
100 - round((features.shape[0] - features_test.shape[0]) * 100 / features.shape[0]), "%" )
print('"target_test":', target_test.shape[0],'объектов,',
100 - round((target.shape[0] - target_test.shape[0]) * 100 / target.shape[0]), "%" )
"features_train": 4718 объектов, 67 % "target_train": 4718 объектов, 67 % "features_test": 2325 объектов, 33 % "target_test": 2325 объектов, 33 %
Определим числовые и категориальные признаки:
numerical = features_train.select_dtypes(include=[np.number]).columns.tolist()
print('Числовые признаки:', numerical)
print()
categorical = features_train.select_dtypes(include='object').columns.tolist()
print('Категориальные признаки:', categorical)
Числовые признаки: ['monthly_charges', 'total_charges', 'duration', 'contract_end'] Категориальные признаки: ['type', 'paperless_billing', 'payment_method', 'senior_citizen', 'partner', 'online_security', 'online_backup', 'device_protection', 'tech_support', 'streaming_tv', 'streaming_movies', 'multiple_lines']
Составим Pipiline для кодирования и масштабирования признаков, при чем OneHotEncoder будет использоваться для линейных моделей, OrdinalEncoder - для моделей на основе дерева решений:
numerical_transformer = StandardScaler()
categorical_transformer_ohe = OneHotEncoder(sparse=False, handle_unknown='ignore')
categorical_transformer_ord_encoder = OrdinalEncoder()
# Делаем ColumnTransformer для категориальных переменных с OrdinalEncoder
preprocessor_ohe = ColumnTransformer(
transformers=[
('num', numerical_transformer, numerical),
('cat', categorical_transformer_ohe, categorical)], verbose_feature_names_out=True)
# Делаем ColumnTransformer для категориальных переменных с OrdinalEncoder
preprocessor_ord_encoder = ColumnTransformer(
transformers=[
('num', numerical_transformer, numerical),
('cat', categorical_transformer_ord_encoder, categorical)], verbose_feature_names_out=True)
Для обучения выбрем следующие модели:
Обучение и оценка моделей будет производится через перекрестную проверку с использованием 3 блоков для кросс-валидации.
%%time
# Составляем pipeline:
pipeline_LR = Pipeline([
("preprocessor_ohe", preprocessor_ohe),
('model_LR', LogisticRegression(random_state=140823,
penalty='elasticnet',
solver='saga',
class_weight='balanced'))
])
param_grid = {'model_LR__C': range(1,15,2),
'model_LR__l1_ratio': np.arange(0, 1, 0.2)}
tuning_model_LR = RandomizedSearchCV(pipeline_LR,
param_grid,
cv=3,
n_jobs=-1,
scoring='roc_auc',
verbose=0,
n_iter=50,
random_state=140823,
)
tuning_model_LR.fit(features_train, target_train)
print(f"Наилучшая метрика 'roc_auc', равная {round(tuning_model_LR.best_score_,2)},\
достигается при параметрах: {tuning_model_LR.best_params_}")
Наилучшая метрика 'roc_auc', равная 0.79,достигается при параметрах: {'model_LR__l1_ratio': 0.0, 'model_LR__C': 1}
CPU times: total: 828 ms
Wall time: 9.06 s
Наилучшая метрика 'roc_auc', равная 0.79,достигается при параметрах:
%%time
# Составляем pipeline:
pipeline_RFC = Pipeline([
("preprocessor_ord_encoder", preprocessor_ord_encoder),
('model_RFC', RandomForestClassifier(random_state=140823,
class_weight='balanced'))
])
param_grid = {
'model_RFC__n_estimators': range (10, 200, 5),
'model_RFC__max_depth': range (1,31, 2),
'model_RFC__min_samples_leaf': range (1,11,2),
'model_RFC__min_samples_split': range (1,21,2)}
tuning_model_RFC = RandomizedSearchCV(pipeline_RFC,
param_grid,
cv=3,
n_jobs=-1,
scoring='roc_auc',
verbose=1,
n_iter=100,
random_state=140823,
)
tuning_model_RFC.fit(features_train, target_train)
print(f"Наилучшая метрика 'roc_auc', равная {round(tuning_model_RFC.best_score_,2)},\
достигается при параметрах: {tuning_model_RFC.best_params_}")
Fitting 3 folds for each of 100 candidates, totalling 300 fits
Наилучшая метрика 'roc_auc', равная 0.88,достигается при параметрах: {'model_RFC__n_estimators': 60, 'model_RFC__min_samples_split': 9, 'model_RFC__min_samples_leaf': 1, 'model_RFC__max_depth': 11}
CPU times: total: 2.84 s
Wall time: 1min 3s
Наилучшая метрика 'roc_auc', равная 0.88, достигается при параметрах:
%%time
pipeline_LGBM = Pipeline([
("preprocessor_ord_encoder", preprocessor_ord_encoder),
('model_LGBM', LGBMClassifier(random_state=140823,
class_weight='balanced'))
])
param_grid = {'model_LGBM__learning_rate': np.arange(0.01, 0.21, 0.05),
'model_LGBM__num_leaves': np.arange(10, 510, 10),
'model_LGBM__max_depth': np.arange(1, 15, 1),
'model_LGBM__n_estimators': range (10, 1000, 10)}
tuning_model_LGBM = RandomizedSearchCV(pipeline_LGBM,
param_grid,
cv=3,
n_jobs=-1,
scoring='roc_auc',
verbose=1,
n_iter=100,
random_state=140823
)
tuning_model_LGBM.fit(features_train, target_train)
print(f"Наилучшая метрика 'roc_auc', равная {round(tuning_model_LGBM.best_score_,2)},\
достигается при параметрах: {tuning_model_LGBM.best_params_}")
Fitting 3 folds for each of 100 candidates, totalling 300 fits
Наилучшая метрика 'roc_auc', равная 0.9,достигается при параметрах: {'model_LGBM__num_leaves': 390, 'model_LGBM__n_estimators': 410, 'model_LGBM__max_depth': 2, 'model_LGBM__learning_rate': 0.16000000000000003}
CPU times: total: 2.64 s
Wall time: 1min 22s
Наилучшая метрика 'roc_auc', равная 0.9, достигается при параметрах:
%%time
pipeline_CBL = Pipeline([
("preprocessor_ord_encoder", preprocessor_ord_encoder),
('model_CBL', CatBoostClassifier(random_state=140823))
])
param_grid = {
'model_CBL__depth': np.arange(1, 15, 1),
'model_CBL__learning_rate': np.arange(0.01, 0.21, 0.05),
'model_CBL__iterations': range (10, 150, 10),
'model_CBL__l2_leaf_reg': np.arange(1, 15, 1)
}
tuning_model_CBL = RandomizedSearchCV(pipeline_CBL,
param_grid,
cv=3,
n_jobs=-1,
scoring='roc_auc',
verbose=0,
n_iter=30,
random_state=140823
)
tuning_model_CBL.fit(features_train, target_train)
print(f"Наилучшая метрика 'roc_auc', равная {round(tuning_model_CBL.best_score_,2)},\
достигается при параметрах: {tuning_model_CBL.best_params_}")
0: learn: 0.6104719 total: 152ms remaining: 18.1s
1: learn: 0.5579133 total: 155ms remaining: 9.15s
2: learn: 0.5103059 total: 176ms remaining: 6.88s
3: learn: 0.4713050 total: 186ms remaining: 5.4s
4: learn: 0.4411582 total: 196ms remaining: 4.5s
5: learn: 0.4134217 total: 209ms remaining: 3.96s
6: learn: 0.3911923 total: 226ms remaining: 3.65s
7: learn: 0.3737332 total: 236ms remaining: 3.3s
8: learn: 0.3583523 total: 243ms remaining: 3s
9: learn: 0.3462995 total: 252ms remaining: 2.77s
10: learn: 0.3354658 total: 262ms remaining: 2.6s
11: learn: 0.3264000 total: 270ms remaining: 2.43s
12: learn: 0.3178844 total: 278ms remaining: 2.29s
13: learn: 0.3112632 total: 285ms remaining: 2.16s
14: learn: 0.3054442 total: 290ms remaining: 2.03s
15: learn: 0.2999026 total: 296ms remaining: 1.93s
16: learn: 0.2951865 total: 302ms remaining: 1.83s
17: learn: 0.2914498 total: 307ms remaining: 1.74s
18: learn: 0.2877603 total: 312ms remaining: 1.66s
19: learn: 0.2847123 total: 318ms remaining: 1.59s
20: learn: 0.2820844 total: 325ms remaining: 1.53s
21: learn: 0.2784046 total: 329ms remaining: 1.47s
22: learn: 0.2760142 total: 334ms remaining: 1.41s
23: learn: 0.2735383 total: 339ms remaining: 1.36s
24: learn: 0.2718490 total: 345ms remaining: 1.31s
25: learn: 0.2699109 total: 349ms remaining: 1.26s
26: learn: 0.2676971 total: 361ms remaining: 1.24s
27: learn: 0.2657717 total: 367ms remaining: 1.21s
28: learn: 0.2647290 total: 375ms remaining: 1.18s
29: learn: 0.2636886 total: 383ms remaining: 1.15s
30: learn: 0.2619089 total: 393ms remaining: 1.13s
31: learn: 0.2605894 total: 397ms remaining: 1.09s
32: learn: 0.2590232 total: 401ms remaining: 1.06s
33: learn: 0.2572242 total: 407ms remaining: 1.03s
34: learn: 0.2559326 total: 411ms remaining: 998ms
35: learn: 0.2545307 total: 415ms remaining: 968ms
36: learn: 0.2533173 total: 419ms remaining: 940ms
37: learn: 0.2519097 total: 423ms remaining: 914ms
38: learn: 0.2510236 total: 427ms remaining: 888ms
39: learn: 0.2500120 total: 432ms remaining: 863ms
40: learn: 0.2487785 total: 436ms remaining: 840ms
41: learn: 0.2471345 total: 440ms remaining: 818ms
42: learn: 0.2459589 total: 444ms remaining: 796ms
43: learn: 0.2443147 total: 449ms remaining: 775ms
44: learn: 0.2435919 total: 453ms remaining: 755ms
45: learn: 0.2425826 total: 459ms remaining: 738ms
46: learn: 0.2418189 total: 463ms remaining: 719ms
47: learn: 0.2409690 total: 467ms remaining: 701ms
48: learn: 0.2392035 total: 472ms remaining: 684ms
49: learn: 0.2385530 total: 477ms remaining: 667ms
50: learn: 0.2371725 total: 481ms remaining: 651ms
51: learn: 0.2361233 total: 486ms remaining: 635ms
52: learn: 0.2350281 total: 491ms remaining: 620ms
53: learn: 0.2342703 total: 495ms remaining: 605ms
54: learn: 0.2334841 total: 500ms remaining: 591ms
55: learn: 0.2320627 total: 505ms remaining: 577ms
56: learn: 0.2304878 total: 509ms remaining: 563ms
57: learn: 0.2296375 total: 514ms remaining: 549ms
58: learn: 0.2285712 total: 518ms remaining: 536ms
59: learn: 0.2274190 total: 523ms remaining: 523ms
60: learn: 0.2267351 total: 527ms remaining: 510ms
61: learn: 0.2257824 total: 532ms remaining: 497ms
62: learn: 0.2252382 total: 536ms remaining: 485ms
63: learn: 0.2241388 total: 540ms remaining: 473ms
64: learn: 0.2232916 total: 545ms remaining: 461ms
65: learn: 0.2225542 total: 549ms remaining: 449ms
66: learn: 0.2221412 total: 554ms remaining: 438ms
67: learn: 0.2211160 total: 562ms remaining: 429ms
68: learn: 0.2199636 total: 567ms remaining: 419ms
69: learn: 0.2189675 total: 572ms remaining: 408ms
70: learn: 0.2185556 total: 576ms remaining: 397ms
71: learn: 0.2182530 total: 581ms remaining: 387ms
72: learn: 0.2174748 total: 589ms remaining: 379ms
73: learn: 0.2170054 total: 595ms remaining: 370ms
74: learn: 0.2162571 total: 601ms remaining: 360ms
75: learn: 0.2154745 total: 605ms remaining: 350ms
76: learn: 0.2146147 total: 611ms remaining: 341ms
77: learn: 0.2141877 total: 616ms remaining: 332ms
78: learn: 0.2135819 total: 620ms remaining: 322ms
79: learn: 0.2125027 total: 625ms remaining: 312ms
80: learn: 0.2116462 total: 629ms remaining: 303ms
81: learn: 0.2109381 total: 633ms remaining: 294ms
82: learn: 0.2103705 total: 638ms remaining: 284ms
83: learn: 0.2098662 total: 643ms remaining: 276ms
84: learn: 0.2089565 total: 647ms remaining: 266ms
85: learn: 0.2080174 total: 652ms remaining: 258ms
86: learn: 0.2074941 total: 656ms remaining: 249ms
87: learn: 0.2071838 total: 661ms remaining: 240ms
88: learn: 0.2061283 total: 665ms remaining: 232ms
89: learn: 0.2050620 total: 669ms remaining: 223ms
90: learn: 0.2043908 total: 673ms remaining: 214ms
91: learn: 0.2036915 total: 677ms remaining: 206ms
92: learn: 0.2027861 total: 682ms remaining: 198ms
93: learn: 0.2023744 total: 687ms remaining: 190ms
94: learn: 0.2018991 total: 691ms remaining: 182ms
95: learn: 0.2014200 total: 695ms remaining: 174ms
96: learn: 0.2007151 total: 699ms remaining: 166ms
97: learn: 0.2001278 total: 704ms remaining: 158ms
98: learn: 0.1995144 total: 709ms remaining: 150ms
99: learn: 0.1990695 total: 713ms remaining: 143ms
100: learn: 0.1982690 total: 719ms remaining: 135ms
101: learn: 0.1980452 total: 723ms remaining: 128ms
102: learn: 0.1973501 total: 727ms remaining: 120ms
103: learn: 0.1970446 total: 731ms remaining: 113ms
104: learn: 0.1967605 total: 736ms remaining: 105ms
105: learn: 0.1963457 total: 740ms remaining: 97.8ms
106: learn: 0.1958531 total: 745ms remaining: 90.5ms
107: learn: 0.1954689 total: 749ms remaining: 83.2ms
108: learn: 0.1945782 total: 753ms remaining: 76ms
109: learn: 0.1940821 total: 758ms remaining: 68.9ms
110: learn: 0.1934222 total: 762ms remaining: 61.8ms
111: learn: 0.1927274 total: 768ms remaining: 54.8ms
112: learn: 0.1920778 total: 772ms remaining: 47.8ms
113: learn: 0.1916859 total: 777ms remaining: 40.9ms
114: learn: 0.1911354 total: 781ms remaining: 34ms
115: learn: 0.1906868 total: 788ms remaining: 27.2ms
116: learn: 0.1902897 total: 796ms remaining: 20.4ms
117: learn: 0.1896907 total: 802ms remaining: 13.6ms
118: learn: 0.1890179 total: 807ms remaining: 6.78ms
119: learn: 0.1886750 total: 811ms remaining: 0us
Наилучшая метрика 'roc_auc', равная 0.89,достигается при параметрах: {'model_CBL__learning_rate': 0.16000000000000003, 'model_CBL__l2_leaf_reg': 2, 'model_CBL__iterations': 120, 'model_CBL__depth': 7}
CPU times: total: 2.58 s
Wall time: 5min 53s
Наилучшая метрика 'roc_auc', равная 0.89, достигается при параметрах:
total = [["LogisticRegression: ", round(tuning_model_LR.best_score_,3)],
["RandomForestClassifier: ", round(tuning_model_RFC.best_score_,3)],
["LightGBMClassifier: ", round(tuning_model_LGBM.best_score_,3)],
["CatBoostClassifier: ", round(tuning_model_CBL.best_score_,3)]
]
total= pd.DataFrame(total, columns=["модель","ROC-AUC"])
total = total.set_index('модель')
total.index.names = [None]
total
| ROC-AUC | |
|---|---|
| LogisticRegression: | 0.790 |
| RandomForestClassifier: | 0.875 |
| LightGBMClassifier: | 0.897 |
| CatBoostClassifier: | 0.894 |
Mодели LightGBMClassifier и CatBoostClassifier оказались наиболее точными по метрике ROC-AUC со значениями 0.897 и 0.894 соответсвенно. Худший результат показала модель линейной регрессии - 0,79. RandomForestClassifier показала значение ROC-AUC, равной 0.875. Лучшей моделью выбираем LightGBMClassifier
best_model = tuning_model_LGBM
Проверим лучшую модель (LightGBMClassifier) на тестовой выборке:
probabilities_test = best_model.predict_proba(features_test)
probabilities_one_test = probabilities_test[:, 1]
auc_roc = roc_auc_score(target_test,probabilities_one_test)
print('Метрика ROC-AUC наилучшей модели равна', round(auc_roc,2))
Метрика ROC-AUC наилучшей модели равна 0.92
Метрика ROC-AUC модели LGBMClassifier на тестовой выборке составила 0.92, что выше условия задания - метрика ROC-AUC на тестовой выборке должна показать 0.85, поэтому цель задания выполнена. Рассмотрим более подробно метрики лучшей модели.
Построим ROC-кривую для лучшей модели и изобразите её на графике:
# Определим долю ложноположительных ответов (FPR) и истинно положительных ответов (TPR) лучшей модели:
fpr, tpr, thresholds = roc_curve(target_test,probabilities_one_test)
# ROC-кривая лучшей модели:
plt.figure(figsize=(8, 5))
sns.lineplot(x=fpr, y=tpr)
# ROC-кривая случайной модели:
sns.lineplot(x=[0, 1],
y=[0, 1],
linestyle='--')\
.set(xlabel='Ложноположительный результат',
ylabel='Истинноположительный результат',
title = 'ROC-кривая')
plt.xlim(0,1)
plt.ylim(0,1)
plt.legend(['Лучшая модель', 'Случайная модель'])
plt.show()
На графике по горизонтали показана доля ложноположительных ответов (False Positive Rate), по вертикали - доля истинно положительных ответов (True Positive Rate). Для модели, которая всегда отвечает случайно, ROC-кривая представлена прямой оранжевой пунктирной линией. Касательно качества модели на графике, если модель не делает ошибок, то кривая будет стремиться к точке (0.0,1.0), в противном случае, AUC-ROC стремиться к 0.5, то есть случайно выдавать вероятность классов. Полученная площадь под кривой метрика AUC-ROC в 0.92 говорит о том, что рассматриваемая модель хорошо предсказывает значения.
Рассчитаем долю правильных ответов лучшей модели для тестовой выборки - Accuracy:
predictions = best_model.predict(features_test)
print('Метрика Accuracy наилучшей модели равна', round(accuracy_score(target_test, predictions),4))
Метрика Accuracy наилучшей модели равна 0.8404
Accuracy показывает отношение количества правильных прогнозов к их общему количеству. Accuracy лучшей модели, равная 0.8404, означает, что модель с точностью 84,04% делает верный прогноз.
Наглядно представить результаты вычислений метрик точности и полноты позволяет матрица ошибок. Матрица ошибок формируется следующим образом: по диагонали от верхнего левого угла выстроены правильные прогнозы, вне главной диагонали — ошибочные варианты и содержит:
cm = confusion_matrix(target_test,predictions)
cm
array([[1665, 297],
[ 74, 289]], dtype=int64)
Визуализируем матрицу ошибок:
plt.figure(figsize=(6, 6))
sns.heatmap(cm, annot=True, linewidth=.5, fmt=".0f", cbar=False)
plt.title("Матрица ошибок", fontsize=20)
plt.xlabel('Предсказания', fontsize=14)
plt.ylabel('Истинный класс', fontsize=14)
plt.show()
Цель задачи состоит в максимизации истинно положительных и истинно отрицательных ответов и минимизации ложноположительных и ложноотрицательных ответов. Лучшая модель LightGBMClassifier выдает 1665 истинно положительных и 289 истинно отрицательных ответов, при этом на долю ложных приходится 297 положительных и 74 отрицательных ответов соответственно.
Выведем таблицу с важностью признаков:
feature_importance = [best_model.best_estimator_[0].transformers_[0][2] + best_model.best_estimator_[0].transformers[1][2],
list(best_model.best_estimator_[1].feature_importances_)]
feature_importance = pd.DataFrame(feature_importance).transpose()
feature_importance.columns =['Признаки', 'Важность']
feature_importance = feature_importance.set_index('Признаки', drop=True).sort_values(by='Важность', ascending=False)
feature_importance
| Важность | |
|---|---|
| Признаки | |
| contract_end | 423 |
| duration | 301 |
| total_charges | 172 |
| monthly_charges | 161 |
| payment_method | 38 |
| type | 32 |
| partner | 22 |
| online_backup | 16 |
| tech_support | 13 |
| multiple_lines | 13 |
| senior_citizen | 12 |
| device_protection | 9 |
| paperless_billing | 7 |
| streaming_movies | 6 |
| online_security | 3 |
| streaming_tv | 1 |
Визуализируем таблицу:
plt.figure(figsize=(8, 4))
sns.barplot(feature_importance, y = feature_importance.index, x = 'Важность')
plt.title('Важность признаков', fontsize = 16)
plt.show()
Таким образом, наибольшее влияние на прогнозирование модели оказывают синтетические признаки: близость окончания ежемесячного договора 'contract_end' (423) и длительность обслуживания клиента 'duration' (301) . Также сильное влияение имеют признаки с количественными значениями: ежемесячные траты на услуги 'monthly_charges' (161) и потраченные деньги на услуги 'total_charges' (172). Наименьшее влияние оказывают услуги онлайн-ТВ (1) и межсетевого экрана (3).
Датасет признаков разделен на обучающие и тестовые выборки в соотношении 3 к 1. Составлен Pipiline для кодирования и масштабирования признаков, при чем OneHotEncoder используется для линейных моделей, OrdinalEncoder - для моделей на основе дерева решений. Для обучения были выбраны следующие модели: LogisticRegression, RandomForestClassifier, LightGBM Classifier, CatBoostClassifier.
Обучение и оценка моделей производилась через перекрестную проверку с использованием 3 блоков для кросс-валидации. Mодели LightGBMClassifier и CatBoostClassifier оказались наиболее точными по метрике ROC-AUC со значениями 0.897 и 0.894 соответсвенно. Худший результат показала модель линейной регрессии - 0,79. RandomForestClassifier показала значение ROC-AUC, равной 0.88.
Лучшей моделью была выбрана LightGBMClassifier с параметрами: 'num_leaves': 390, 'n_estimators': 410, 'max_depth': 2, 'learning_rate': 0.16. Метрика ROC-AUC модели LGBMClassifier на тестовой выборке составила 0.92, что выше условия задания - метрика ROC-AUC на тестовой выборке должна показать 0.85, поэтому цель задания выполнена. Дополнительно построена ROC-кривая для модели.
Рассчитана доля правильных ответов Accuracy, равная 0.84, построена матрица ошибок. Согласно матрицы ошибок, модель LightGBMClassifier выдает 1665 истинно положительных и 289 истинно отрицательных ответов, при этом на долю ложных приходится 297 положительных и 74 отрицательных ответов соответственно.
Рассмотрена важность признаков. Наибольшее влияние на прогнозирование модели оказывают синтетические признаки: близость окончания ежемесячного договора 'contract_end' (423) и длительность обслуживания клиента 'duration' (301). Также сильное влияение имеют признаки с количественными значениями: ежемесячные траты на услуги 'monthly_charges' (161) и потраченные деньги на услуги 'total_charges' (172). Наименьшее влияние оказывают услуги онлайн-ТВ (1) и межсетевого экрана (3).
# Функции для построения графиков
# Анализ дисбаланса классов
def class_imbalance(data, target):
(data[target].value_counts()/data[target].count()*100).plot(
kind='bar',
grid = True
).set(
ylabel = 'Относительное количество, %',
xlabel = 'Факт ухода клиента: \n "No" - Клиент не ушел, "Yes" - Клиент ушел',
title = 'Распределение оставшихся и ушедщих клиентов'
)
plt.show()
# Анализ признаков с количественными значениями
def analysis_сat_report(data, cat_col_list, cat_name_list):
for i in range(15):
plt.figure(figsize=(12,4))
for b in data[col_list[i]].unique():
plt.subplot(1,len(data[col_list[i]].unique()),list(data[col_list[i]].unique()).index(b)+1)
sns.histplot(data=data[data[col_list[i]] == b], x='leave', hue ='leave',
shrink = 0.8, multiple='dodge', stat = 'percent', bins = 4)
plt.title(str(i+1)+'.'+ name_list[i] + '\n- ' + b, fontsize=12)
plt.tight_layout()
# Анализ признаков с категориальными значениями
def analysis_num_report(data, num_list):
for i in num_list:
print()
print('\033[1m' + i,':' +'\033[0m')
print()
sns.set()
lines, axes = plt.subplots(1, 2, figsize=(16, 4))
sns.histplot(data=data, hue='leave', x=i, ax=axes[0], kde=True).set_title(i + ". Плотность распределения", fontsize=18)
sns.boxplot(data=data, x=i, y='leave', ax=axes[1]).set_title(i + ". Диаграмма размаха", fontsize=18)
plt.show()
# Создание списков для функций
cat_col_list = ['type', 'paperless_billing', 'gender', 'senior_citizen',
'partner', 'dependents', 'payment_method', 'internet_service', 'online_security', 'online_backup',
'device_protection', 'tech_support', 'streaming_tv', 'streaming_movies', 'multiple_lines']
cat_name_list = ['Тип договора', 'Выставления счёта по электронной почте', 'Пол клиента', 'Пенсионный статус',
'Наличие супруга/супруги', 'Наличие иждивенцев', 'Способ оплаты', 'Наличие услуг Интернет', 'Межсетевой экран',
'Облачное хранилище файлов', 'Антивирус', 'Выделенная линия технической поддержки',
'Онлайн-ТВ', 'Онлайн-кинотеатр', 'Возможность подключения телефонного \n аппарата к нескольким линиям']
num_list = ['monthly_charges','total_charges', 'duration', 'contract_end']
В процессе выполнения работы были выполнены следующие задачи:
Задачи выполнены полностью в соответсвии с планом, за исключением: Удаление признаков осуществлялось также и после корреляционного анализа; На этапе кодирования и масштабирования признаков был написан конвейер (Pipline) для этой цели, а сам процесс производился на следующем этапе при обучении моделей на кросс-валидации.
Основные трудности, возникшие при выполнении работы, связана с предобработкой исходных данных, из которых следует выделить:
class_imbalance(df_before_delete, 'leave')
analysis_сat_report(df_before_delete, cat_col_list, cat_name_list)
analysis_num_report(df_before_delete, num_list)
monthly_charges :
total_charges :
duration :
contract_end :
Рассматривая распределения признаков в разрезе целевой переменной, можно отметить:
plot_correlation_matrix(phik_overview.values, x_labels=phik_overview.columns, y_labels=phik_overview.index,
vmin=0, vmax=1, color_map='Blues', title=r'correlation $\phi_K$', fontsize_factor=1.5,
figsize=(16,20))
plt.tight_layout()
Итоговый датасет содержал 16 признаков и 7043 объекта, пропущенных значений нет.
total
| ROC-AUC | |
|---|---|
| LogisticRegression: | 0.790 |
| RandomForestClassifier: | 0.875 |
| LightGBMClassifier: | 0.897 |
| CatBoostClassifier: | 0.894 |
Гиперпараметры подбирались методом рандомизированного поиска по параметрам - RandomizedSearchCV с бюджетом вычислений n_iter, равным 100. Выбранное распределение по возможным значениям гиперпараметров представлено ниже:
plt.figure(figsize=(8, 5))
sns.lineplot(x=fpr, y=tpr)
sns.lineplot(x=[0, 1],
y=[0, 1],
linestyle='--')\
.set(xlabel='Ложноположительный результат',
ylabel='Истинноположительный результат',
title = 'ROC-кривая')
plt.xlim(0,1)
plt.ylim(0,1)
plt.legend(['Лучшая модель', 'Случайная модель'])
plt.show()
На графике по горизонтали показана доля ложноположительных ответов (False Positive Rate), по вертикали - доля истинно положительных ответов (True Positive Rate). Для модели, которая всегда отвечает случайно, ROC-кривая представлена прямой оранжевой пунктирной линией. Касательно качества модели на графике, если модель не делает ошибок, то кривая будет стремиться к точке (0.0,1.0), в противном случае, AUC-ROC стремиться к 0.5, то есть случайно выдавать вероятность классов. Полученная площадь под кривой метрика AUC-ROC в 0.92 говорит о том, что рассматриваемая модель хорошо предсказывает значения классов.
plt.figure(figsize=(6, 6))
sns.heatmap(cm, annot=True, linewidth=.5, fmt=".0f", cbar=False)
plt.title("Матрица ошибок", fontsize=20)
plt.xlabel('Предсказания', fontsize=14)
plt.ylabel('Истинный класс', fontsize=14)
plt.show()
Цель задачи состоит в максимизации истинно положительных и истинно отрицательных ответов и минимизации ложноположительных и ложноотрицательных ответов. Выбранная модель LightGBMClassifier выдает 1665 истинно положительных и 289 истинно отрицательных ответов, при этом на долю ложных приходится 297 положительных и 74 отрицательных ответов соответственно.
plt.figure(figsize=(8, 4))
sns.barplot(feature_importance, y = feature_importance.index, x = 'Важность')
plt.title('Важность признаков', fontsize = 16)
plt.show()
Наибольшее влияние на прогнозирование модели оказывают синтетические признаки: близость окончания ежемесячного договора 'contract_end' (423) и длительность обслуживания клиента 'duration' (301). Также сильное влияение имеют признаки с количественными значениями: ежемесячные траты на услуги 'monthly_charges' (161) и потраченные деньги на услуги 'total_charges' (172). Наименьшее влияние оказывают услуги онлайн-ТВ (1) и межсетевого экрана (3).
Выбранная модель машинного обучения LightGBMClassifier эффективно справляется с поставленой задачей предсказания ухода клиентов от оператора связи «Ниединогоразрыва.ком» со значением метрики ROC-AUC в 0.92, что выше поставленной задачи - "метрика ROC-AUC на тестовой выборке должна составлять не менее 0.85". Высокое качество предсказания модели получилось достичь за счет устранения мультиколлиарности, удаления избыточных признаков и признаков имеющих высокую корреляцию с целевым признаком. Для предотвращения утечки данных в модели используются конвейеры (pipeline) для кодирования и масштабирования признаков.
Так как модель позволяет предсказывать с высокой точностью уход клиентов, предоставляется возможность заказчику в соответсвии с предсказанием применять действия по удержанию клиентов, например, выдавать промо-купоны, скидки и прочие пакеты стимулирования.
Также модель дала возможность определить наиболее важные признаки: близость окончания ежемесячного договора, длительность обслуживания клиента, ежемесячные траты на услуги, потраченные деньги на услуги. Заказчику рекомендуется обратить внимание на эти признаки, влияя на которые можно снизить вероятность ухода клиентов.
Модель машинного обучения рекомендуется к вводу в экслуатацию.